Zig
Impressions
-
(2025-03-30)
Negative points
-
The LSP is TERRIBLE :
-
.
-
The parameters of
readAllAllocare:self,allocatorandsize. -
The parameters shown by the inlay hint are wrong, because it considers the first parameter to be
self, when in fact that is passed implicitly sincereadAllAllocis a method of a struct. -
statwasn't unwrapped in this case, so it will cause an error when trying to dostat.size-
In other words, it never informs me that I'm forgetting to do error handling.
-
-
-
It keeps showing me errors that have already been resolved after compiling and getting an error.
-
Even after fixing the error and even after restarting the LSP, it stays stuck showing old errors and ignoring the new ones.
-
-
-
Zig output messages are horrible and confusing .
-
Anonymous Structs are unpleasant .
-
Safety :
-
I don't like how anonymous structs are called; it always feels like I'm dealing with some Lua object.
-
There is NO indicator that I'm forgetting to initialize some required struct parameter.
-
The LSP doesn't try to infer which anonymous struct I'm referring to, to give me information whether things are correct.
-
Maybe that's very hard, since the struct is completely anonymous and could mean anything.
-
-
-
I don't like the syntax :
-
The declaration is done like this:
const Player = struct { pos: rl.Vector2, vel: rl.Vector2 = rl.Vector2.init(0, 0), aceleracao: f16, vel_max: f16, sprite: rl.Texture2D };-
I find it confusing where the
structkeyword is placed.-
I'm creating a TYPE, but the impression I get is that I'm just creating a random
constthat receives an anonymous struct, but using thestructkeyword.-
Maybe deep down that's what is happening, but it's odd. I want a type definition, not something "taken apart" like this.
-
-
In Odin structs are declared the same way, but in Odin the
keywordfollows the same pattern as the declaration ofproc:Vector2 :: struct { x: f32, y: f32, }-
Proc in odin:
multiply :: proc(x: int, y: int) -> int { return x * y } -
It follows the same structure
name+::(const) +keyword(proc or struct) + syntax.
-
-
Zig seems undecided about it, such that it uses
var + name,const + nameandfn + namefor some things, but decides to invert the syntax for struct:const + name + = + struct.
-
-
-
But instantiation is done like this:
var player: Player = .{ .pos = rl.Vector2.init(0, 0), .aceleracao = 100, .vel_max = 4, .sprite = player_sprite, };-
I don't like how instantiation uses
.in front of the names.-
I always forget to put them because it's just another small rule.
-
Maybe there is an argument about this, putting structs in a family similar to enums, but still it's annoying the difference between field definition and field access.
-
-
-
-
There are a zillion ways to instantiate an object :
-
Many of these ways result in bad behavior.
-
This is described in the Structs section.
-
-
-
Error handling :
-
Tiring :
-
tryends up being used in many places, so all allocation operations usetry, since they can return out-of-memory. -
Frequent use of error handling increases the line length to be written, because I always have to:
-
With try
-
Use
try. -
Write the huge function name coming from some package.
-
Pass the allocator as a function parameter.
-
-
Use catch.
-
Write the huge function name coming from some package.
-
Pass the allocator as a function parameter.
-
Use a catch with a huge syntax while defining what to do in case of error.
-
Considering this results in the program not crashing: I have to deal with all local memory deallocations using
errdefer.
-
-
-
Maybe in Go/Odin it's ok, idk, maybe it's tiring there too.
-
Though in Go or Odin errors seem more "soft", since they are just values, but in Zig it's much more "collapsing" and rigid, always feeling like asserts.
-
-
It's necessary that I watch if the function return has
!in Zig.-
In Go or Odin I must watch if it's a double return, so the second return can be an error.
-
-
-
Using try everywhere is basically making an assert, but MUCH more confusing :
-
Under these conditions,
trydoesn't actually provide error handling. Thecatchprovides error handling, in my opinion. -
From what I see,
tryis simply annoying. -
Suppose what happens in Godot:
-
I try an operation that can return an object or null.
-
If it returns the object, OK.
-
If it returns null, then I may:
-
Assert if the object is null, where it should never be null.
-
Crash the code when trying to access some property or method of that object while it's null.
-
Check if the object is null, clearly deal with the consequences, returning from the function or assigning a default value to the object.
-
-
-
So my options are absolutely clear.
-
-
THO, in Zig I feel there is a fourth option:
4. Automatically return the function and propagate that error to a higher function.
- Maybe that error is interpreted nicely in the upper functions, which is unlikely.
- Maybe that error simply keeps being propagated infinitely up to the main loop, which stops execution by forcing the main function to return with an error code.
- That is what happens in the vast majority of cases, since there's simply so much error handling happening in Zig, that 90% of them aretrys, out of laziness.
- In that case, it's basically an assert, but without guarantees that it will actually "assert" and stop the code. You need to traverse the whole call stack to find where error handling actually happens. -
tryis: "I don't want to deal with this error, I will pass the responsibility to whoever receives my return".-
As mentioned,
tryis just a shortcut forx catch |err| return err, which describes exactly what I said, which I find confusing to work with.
-
-
-
-
The std is terrible to understand :
-
.
-
ArrayList:-
It's a function that returns a type.
-
Calling
ArrayListis nothing more than callingArrayListAligned, but without alignment.-
Reflect on that....
-
I'll have to deal with the concept of Alignment, without using any Alignment....
-
-
-
ArrayListAligned.-
It's a function that returns a type.
-
It's a public function that returns a type.
-
It first receives a
typeT as a parameter and alignment information. -
It receives a comptime alignment, which is a
?u29, that is,nullor au29(29 bytes).-
Why is
comptimebefore the parameter name? I find that confusing. I prefer something likealignment: comptime ?u29.
-
-
-
Then the logic: if
alignmentis not null and the alignment equals T's alignment, then callArrayListAlignedagain, but this time with alignment equal to null. -
Then it finally returns a struct containing the type I want to define.
-
-
type
ArrayListAligned(T, alignment)-
Has 3 fields with no default value.
-
A
pub const Slicethat is not accessible outside the struct.-
I really have no idea why this field is not accessible.
-
The LSP doesn't give autocomplete and when trying to access ignoring the LSP I receive an error saying that this field doesn't exist, it's really strange.
-
-
A pub fn that returns a type
SentinelSlice(s). -
A pub fn that returns a type, where this type is
ArrayListAligneditself, that is, a "constructor" function.-
This returns the default values of each of the struct fields.
-
-
-
If the
init(constructor) ofArrayListAlignedonly does this, why weren't these default values already defined in the struct, instead of needing a constructor?-
It sounds like something done only by convention, but at the same time there were moments where this is not necessary or possible:
-
,
-
DebugAllocator is ultra common to be used, it has a total of 863 lines of code, with 6 fields in the struct.
-
It DOES have a
deinit(), but does not have aninit(). -
THO Fun Fact! Reading the comment at the top of the function, it describes that
DebugAllocatordoes have an init (lol).-
If you look well at the screenshot,
initis actually apub const init: Self = .{}. -
.................bro what a mess.
-
-
-
Anyway, the way normally used is:
-
var debugAllocator = std.heap.DebugAllocator(.{}){}; -
or
var debugAllocator = std.heap.DebugAllocator(.{}).init;, if you want to use Zig's "convention".
-
-
-
-
The annoying thing is that trying to follow a pattern in Zig always produces these weird things, because of dealing with anonymous structs without LSP.
-
Much of the complexity is due to OOP; methods are really a pain.
-
All packages I've encountered are written in OOP, using methods for everything. It's simply mega confusing.
-
In the end, I never know when I should initialize the struct with default values using anonymous structs, or if I should call some init function that assigns default parameters to the anonymous struct.
-
-
-
String manipulation is unpleasant :
-
I find dealing with low-level memory fun. The theory is very interesting and I like having control over these things.
-
BUT, I feel Zig makes the whole process a pain, because of how the tools are provided.
-
.
-
In this small segment, it was necessary to use:
-
String comparison.
if (std.mem.eql(u8, info_tileset.get("identifier").?.string, "Internal_Icons")) continue;-
I mean, wtf, it sounds absurd not being able to do:
if (u8, info_tileset.get("identifier").?.string == "Internal_Icons") continue;-
In Odin it's possible to do that.
-
-
-
String concatenation:
const pathCompleto: [:0]const u8 = try std.fmt.allocPrintZ(allocator, "{s}{s}", .{cwd, path}); defer allocator.free(pathCompleto);-
try-
Obviously dealing with error handling
-
-
std.fmt.allocPrintZ-
What a crappy syntax is that?
-
I have to access the standard library, the fmt package and use
allocPrintZ.-
I can't even use
allocPrint, since I need the string to be null terminated.
-
-
-
allocator-
I have to pass the allocator everywhere.
-
-
"{s}{s}"-
Pass the string format, including
sto ensure it is interpreted as a string (as if that wouldn't already be obvious in this case).
-
-
.{cwd, path}-
Anonymous struct without names (I find that very confusing).
-
Anonymous structs are very strange.
-
Sometimes I interpret them as hashmaps, but in this case I could interpret as an array, or hashmap with unnamed keys.
-
-
Despite the confusion with the struct, this part is OK.
-
-
free-
I have to deallocate the memory.
-
-
Damn, it's a lot.
-
This could be simplified a lot.
-
The reason it's so long is because function
allocPrintZorallocPrintreturns[:0]u8, and not[:0]const u8. -
I believe if there were a function that returned
[:0]const u8, then there would be no need to use an allocator as a function parameter, nor to use free after creating the string.-
Memory could be created inside the function, something like that, I don't understand memory well enough, but I believe it's possible to minimize problems by returning a string literal.
-
-
-
-
-
-
-
Print is annoying :
-
I have to use
\nevery time, which I find annoying.-
There was never a moment when I didn't need
\nin print, never.
-
-
-
Ifs and loops use
( ):-
It's quite annoying to have to write
( )around the expressions of ifs and loops.-
I always forget and have to go back to fix it.
-
if (valor != 5) { return; } for (meu_array.items) |item| { std.debug.print("{any}", .{item}); } -
-
Unused parameters being an error is annoying :
-
Sometimes I just want to test something and see if it works, but every time I have to use
_ = a;or_ = .{a, b};. -
This is annoying after a while.
-
It also makes the code cluttered:
-
There will be extra lines of code that do absolutely nothing, they just take up space.
-
-
It also makes the code unsafe:
-
I would prefer it to be a warning, but this became NOTHING.
-
All information about the problem is lost, once I'm completely ignoring the problem.
-
-
In general, I don't like how Zig handles this; it's bad.
-
-
The
usingkeyword does nothing and will be removed :-
This is not a big problem, since I don't find
usingvery welcome, but it would still be ok to have this option for helper functions.
-
-
Constant breaking changes that make using external libs a headache
-
Many libs use incompatible versions among themselves.
-
Many libs use nightly versions, on master; others use older versions because they're not being updated.
-
Many libs even use dev, unstable versions.
-
The problem happens when there are changes in the build system from one version to another, so that it's not possible to use the lib at all unless you download its source code and edit it.
-
-
The build.zig and build.zig.zon are extremely confusing and inconvenient :
-
The LSP doesn't work if all dependencies are not correct in build.zig and build.zig.zon.
-
That's a pain for new projects.
-
-
I tried to learn about build.zig and despite having understood how to configure what I wanted, it's simply very confusing still. It's not friendly for beginners.
-
Maybe the configurations are useful when you master what's happening, but it's unpleasant to poke at those files at the beginning.
-
-
Doesn't do default initialization to Zero Value :
-
Doing
const flag: bool;gives the error "Variables must be initialized".-
This happens for everything.
-
-
Both Odin and GDScript do zero initialization for all types.
-
This is very nice, saves typing and makes the code safer by preventing trying to access an uninitialized property.
-
-
-
Doesn't have a context allocator / default allocator :
-
This can be seen as an advantage, but also as a disadvantage.
-
The disadvantage I see is having to carry the allocator everywhere in the code, leaving the code very loaded, always being "haunted" by memory management.
-
The advantage is the reinforcement that the only way to allocate something is by passing the allocator as a function parameter.
-
EXCEPT, that is false. It's possible to create allocators inside functions however you want. The above sentence is just a convention adopted by Zig.
-
It's a good convention, but still just a convention.
-
-
Anyway, I miss Odin's
context, which saves me the work of dealing with allocators, but allows overriding the allocator used in the function if I want.-
meu_procedure :: proc(allocator := context.allocator) {}-
That's the convention in Odin; the default is the context's allocator, but one can use another allocator if desired.
-
-
-
-
I find mandatory
;slightly annoying :-
Odin doesn't force it and I never missed it at any time.
-
-
Zig's syntax is VERY confusing :
-
No matter how much I use Zig, I feel like the next day I'm very confused about how the syntax works.
-
The entire std is just very confusing and the syntax is full of things I need to have memorized.
-
I reached this conclusion after trying Odin and realizing how much easier and more intuitive Odin's syntax is.
-
I have considerably fewer doubts writing Odin, although I barely read Odin's documentation, while I devoured articles and articles about Zig.
-
-
It sounds strange how unintuitive Zig can be syntactically.
-
I feel Zig has some "fussy bits" that make the syntax one of the most confusing I've used.
-
Rust, Odin, Swift, Jai, etc.; are all syntactically more intuitive than Zig.
-
-
Positive points
-
The return of a JSON parse is ok :
-
The syntax below is ok:
const defs = ldtkParsed.get("defs").?.object; const info_tilesets = defs.get("tilesets").?.array; const path = info_tileset.get("relPath").?.string;
-
-
Unions for Null Safety and Error Handling :
-
Cool.
-
?and!are ok. -
They are aspects of Functional Programming.
-
-
Pointers are nice :
-
The use of
.*to dereference is pretty nice, I liked it.
-
-
pub fnis nice and simple . -
Clarity about what is built-in using
@. -
deferworking in scope is nice .-
I have big doubts about this, though.
-
Go makes
deferwork at the end of the current function scope, which may be better for error handling.-
I found it strange to use
deferinsideifs, as it makes the call happen at the end of theifscope, which is odd and impractical.
-
-
On the other hand, it's nice to create blocks with internal defers:
{ //something here }
-
-
Many things are expressions, instead of only statements :
-
I'm unsure whether I like this so far.
const s = 'a'; const valor: i32 = switch (s) { 'a' => 1, 'b' => 20, 'c' => 5, _ => 0, }; -
About
-
-
Good guide, but very concise.
-
-
Guide .
-
~ ZigGuide .
-
It's a confusing and not very explanatory guide, very concise and technical.
-
-
-
It's an okay place to test some things, but many times I felt lazy to do the exercise, because the statement is looong and Dora the Explorer style, while other times it's not exactly clear what should be done, or why that was done, or what other options exist.
Style Guidelines
-
snake_case
-
variables.
-
namespace.
-
-
camelCase
-
functions.
-
-
PascalCase
-
types.
-
HTTP Requests Example
const std = @import("std");
const http = @import("zig-http/http");
pub fn main() !void {
const allocator = std.heap.page_allocator;
// Create the HTTP server
var server = try http.Server.init(allocator, "0.0.0.0", 8080, handler);
std.debug.print("Server running at http://localhost:8080\n", .{});
// Start the server
try server.listen();
}
// Function that handles HTTP requests
fn handler(req: *http.Request, res: *http.Response) !void {
// Process only GET requests
if (req.method != .Get) {
res.status = .MethodNotAllowed;
try res.send("Method not allowed");
return;
}
// Get the "name" parameter from the URL
const name = req.getQueryParam("name") orelse "world";
// Create the response message
const message = try std.fmt.allocPrint(std.heap.page_allocator, "Hello, {}!", .{name});
// Set status and response body
res.status = .Ok;
try res.send(message);
// Free the memory allocated for the message
std.heap.page_allocator.free(message);
}
Installation
Version
-
Use Scoop or download the standalone executable.
-
I placed the downloaded folder in
C:\Users\caior\appsand added it to the user path.
-
-
For dev versions, often it only works via the executable.
-
The exercises repo and the gamedev examples repo only use the dev version, so it makes more sense to use master right away.
-
-
I installed via.winget
VSCode
-
Installing Zig, the Zig extension in VSCode and the LSP in VSCode was smooth, no problem at all.
-
.
-
Apparently you have to do this for the debugger to work in vscode.
-
(2025-03-15) I did this, the same way as in the image.
-
Build
-
Build .
-
Build .
-
I found the
build.zigfile very useful, making it clear what will happen when building the project. -
zig build -hshows the options created by thebuild.zigfile. -
Some brief explanations of RayLib in Zig with and without bindings .
-
One of the shown options is to use RayLib with
@cImport
-
flags
-
--prefix/-p
.zig-cache
-
Contains files that will make subsequent builds faster
-
These files are not intended to be checked into source-control and this directory can be completely deleted at any time with no consequences.
build.zig.zon
-
In my understanding, the file contains the pathing of dependencies to be downloaded in order to build the project.
-
When running
zig run
-
Compiles the specified source code directly into a temporary binary.
-
Runs the binary immediately after compilation.
-
Does not use the build system, that is, it doesn't process the configurations defined there.
Resolving version conflicts
Comparisons
-
0.13.0 -> 0.14.0
-
Introduces problems in the
.namedescribed in thebuild.zig.zonfile, so that names must be in the format.my_name, I think. -
Although this can be changed in the project itself, all dependencies end up having this problem as well.
-
-
FunFacts!!
-
Some dependencies use a 0.14.0-dev version, so 0.13.0 is incompatible and 0.14.0 introduces problems with
.nameCoooool! =)
-
Running demos on different versions
-
Remember to use
.before the Zig path. -
."C:\Users\caior\apps\zig-windows-x86_64-0.14.0\zig.exe" build run -
."C:\Users\caior\apps\zig-windows-x86_64-0.13.0\zig.exe" build run -
."C:\Users\caior\apps\zig-windows-x86_64-0.12.1\zig.exe" build run
Basics
Operators
-
List .
Keywords
Access Modifiers
-
Chat-gpt: " Default: There is no native visibility control."
-
pubis related to code organization and modularity, but is not tied to dynamic runtime security rules . -
Zig uses modular organization to control access between different parts of the code.
-
Public (
pub) : Used to expose functions, types or variables from a module to other modules. -
Private (default) : Items defined without
pubare private to the current scope (file or block)."
-
Comments, Print, Formatting
Comments
//! Top-level documentation.
/// Documentation comment.
// Simple comment.
Suppress Warnings
//suppress unused constant compile error
_ = .{ a, b, c, d };
-
No automatic
\n. -
"Prints to stderr (it's a shortcut based on
std.io.getStdErr())". -
Part of the standard library (
std). -
Uses
std.debug.print(). -
Writes directly to standard output (
stdout). -
Accepts string formatting, similar to
printfin C.
const std = @import("std");
const print = std.debug.print
pub fn main() void {
print("Value: {}\n", .{42});
}
Info
-
With automatic
\n. -
Part of the
std.logmodule. -
Uses
std.log.info(). -
Unlike
print,infocan be filtered by log levels (likedebug,warn,err). -
Output can be redirected or configured depending on the compiler and runtime environment.
const std = @import("std");
pub fn main() void {
std.log.info("Value: {}", .{42});
}
Formatting
-
std.fmtprovides ways to format data to and from strings. -
A basic example of creating a formatted string. The format string must be compile-time known. The
dhere denotes that we want a decimal number.
const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
const test_allocator = std.testing.allocator;
test "fmt" {
const string = try std.fmt.allocPrint(
test_allocator,
"{d} + {d} = {d}",
.{ 9, 10, 19 },
);
defer test_allocator.free(string);
try expect(eql(u8, string, "9 + 10 = 19"));
}
-
std.debug.print :
-
"it writes to stderr and is protected by a mutex."
const std = @import("std"); const expect = std.testing.expect; const eql = std.mem.eql; test "hello world" { const out_file = std.io.getStdOut(); try out_file.writer().print( "Hello, {s}!\n", .{"World"}, ); } -
-
Format Specifiers :
-
{s}: strings. -
{d}: decimal.-
{d:.2}
-
-
{c}: ascii character. -
{*}: pointer formatting, printing the address rather than the value. -
{any}: default formatting. -
{e}: floats in scientific notation. -
{b}: binary. -
{o}: octal. -
etc.
-
Functions
-
All function arguments are immutable - if a copy is desired the user must explicitly make one.
-
Unlike variables, which are snake_case, functions are camelCase.
fn addFive(x: u32) u32 {
return x + 5;
}
test "function" {
const y = addFive(0);
}
Built-in Functions
Control Flow (if, while, for, switch, labelled, iterators)
If
const a = true;
var x: u16 = 0;
if (a) {
x += 1;
} else {
x += 2;
}
const a = true;
var x: u16 = 0;
x += if (a) 1 else 2;
-
"If it exists" :
// Shortcut for "if (x) x else 0" var value = x orelse 0;// Get a pointer to the value (if it exists). if (a) |*value| { value.* += 1; }
While
var i: u8 = 2;
while (i < 100) {
i *= 2;
}
// Simple "while" loop.
while (i < 10) { i += 1; }
// While loop with a "continue expression"
// (expression executed as the last expression of the loop).
while (i < 10) : (i += 1) { ... }
// Same, with a more complex continue expression (block of code).
while (i * j < 2000) : ({ i *= 2; j *= 3; }) { ... }
var sum: u8 = 0;
var i: u8 = 1;
while (i <= 10) : (i += 1) {
sum += i;
}
var sum: u8 = 0;
var i: u8 = 0;
while (i <= 3) : (i += 1) {
if (i == 2) continue;
sum += i;
}
var sum: u8 = 0;
var i: u8 = 0;
while (i <= 3) : (i += 1) {
if (i == 2) break;
sum += i;
}
-
Loops as Expressions :
-
Like
return,breakaccepts a value. -
This can be used to yield a value from a loop.
-
Loops in Zig also have an
elsebranch, which is evaluated when the loop is not exited with abreak.
fn rangeHasNumber(begin: usize, end: usize, number: usize) bool { var i = begin; return while (i < end) : (i += 1) { if (i == number) { break true; } } else false; } test "while loop expression" { try expect(rangeHasNumber(0, 10, 3)); } -
For
// We've had to assign values to `_`, as Zig does not allow us to have unused values.
// Character literals are equivalent to integer literals
const string = [_]u8{ 'a', 'b', 'c' };
for (string, 0..) |character, index| {
_ = character;
_ = index;
}
for (string) |character| {
_ = character;
}
for (string, 0..) |_, index| {
_ = index;
}
for (string) |_| {}
// To iterate over a portion of a slice, reslice.
for (items[0..1]) |value| { sum += value; }
// Loop over every item of an array (or slice).
for (items) |value| { sum += value; }
// Iterate and get pointers on values instead of copies.
for (items) |*value| { value.* += 1; }
// Iterate with an index.
for (items) |value, i| { print("val[{}] = {}\n", .{i, value}); }
// Iterate with pointer and index.
for (items) |*value, i| { print("val[{}] = {}\n", .{i, value}); value.* += 1; }
// Break and continue are supported.
for (items) |value| {
if (value == 0) { continue; }
if (value >= 10) { break; }
// ...
}
// For loops can also be used as expressions.
// Similar to while loops, when you break from a for loop,
// the else branch is not evaluated.
var sum: i32 = 0;
// The "for" loop has to provide a value, which will be the "else" value.
const result = for (items) |value| {
if (value != null) {
sum += value.?; // "result" will be the last "sum" value.
}
} else 0; // Last value.
Switch
-
Safety :
-
The types of all branches must coerce to the type which is being switched upon. All possible values must have an associated branch - values cannot be left out. It is exhaustive.
-
-
Zig's
switchworks as both a statement and an expression.-
Statement :
const expect = @import("std").testing.expect; test "switch statement" { var x: i8 = 10; switch (x) { -1...1 => { x = -x; }, 10, 100 => { //special considerations must be made //when dividing signed integers x = @divExact(x, 10); }, else => {}, } try expect(x == 1); } -
Expression :
const expect = @import("std").testing.expect; test "switch expression" { var x: i8 = 10; x = switch (x) { -1...1 => -x, 10, 100 => @divExact(x, 10), else => x, }; try expect(x == 1); }
-
-
Cases cannot fall through to other branches.
Labelled
-
Blocks
-
The value of an empty block
{}is a value of the typevoid.
const expect = @import("std").testing.expect; test "int-float conversion" { const a: i32 = 0; const b = @as(f32, @floatFromInt(a)); const c = @as(i32, @intFromFloat(b)); try expect(c == a); }-
This can be seen as being equivalent to C's
i++.
blk: { const tmp = i; i += 1; break :blk tmp; } -
-
Loops :
-
Loops can be given labels, allowing you to
breakandcontinueto outer loops.
test "nested continue" { var count: usize = 0; outer: for ([_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }) |_| { for ([_]i32{ 1, 2, 3, 4, 5 }) |_| { count += 1; continue :outer; } } try expect(count == 8); } -
Iterators
Imports
-
The built-in function
@importtakes in a file, and gives you a struct type based on that file. -
All declarations labelled as
pub(for public) will end up in this struct type, ready for use. -
@import("std")is a special case in the compiler, and gives you access to the standard library.
Types
Casting
@prtCast
-
We can use
@ptrCastto create a new variable that points to the same location but as a different type. -
Ex1 :
const std = @import("std"); const User = struct { id: u32, name: []const u8, }; const Node = struct { next: ?*Node, }; pub fn main() !void { var user1 = User{.id = 1, .name = "Leto"}; const node1: *Node = @ptrCast(&user1); node1.next = null; std.debug.print("{}\n", .{node1}); }-
This code not only compiles, but it also runs. Compiling and running are two distinct aspects we must consider. The code compiles because we told the compiler it was ok to treat the memory as a
*Node.@ptrCastisn't changing the memory at runtime, it's forcing the compiler to see the memory as a*Node. In this case, the code runs because there are some truths we can rely on that make it so the memory used to represent aUsercan safely be used to represent aNode.
-
-
Ex2 :
const std = @import("std"); const User = struct { id: u32, name: []const u8, }; const Node = struct { next: ?*Node, }; pub fn main() !void { var node1 = Node{.next = null}; const user: *User = @ptrCast(&node1); std.debug.print("Id: {d}\n", .{user.id}); std.debug.print("Name: {d}\n", .{user.name}); }-
Now we're creating a
Nodeand telling the compiler to see the underlying memory as aUser. Again, this code compiles. But what happens when we try to run it? You'll probably get the same thing I did:Id: 0followed by a segfault. -
Why does it work one way but not the other? Consider the size of a
Nodeand the size of aUser:
const std = @import("std"); pub fn main() !void { std.debug.print("Node: {d} User: {d}\n", .{@sizeOf(Node), @sizeOf(User)}); }-
Assuming you're on a modern platform, you'll likely see:
Node: 8 User: 24. -
This highlights the power and danger of
@ptrCast: it's obvious that the memory underlying aNodeisn't big enough to represent a wholeUser, but@ptrCastforces the compiler to proceed as though it can. -
But size constraints aren't the only issue. Let's go back to our original example and add 2 more lines at the end:
const std = @import("std"); const User = struct { id: u32, name: []const u8, }; const Node = struct { next: ?*Node, }; pub fn main() !void { var user1 = User{.id = 1, .name = "Leto"}; const node1: *Node = @ptrCast(&user1); node1.next = null; std.debug.print("{}\n", .{node1}); std.debug.print("{d}\n", .{user1.id}); // added std.debug.print("{s}\n", .{user1.name}); // added }-
The underlying memory for
node1is more than big enough, but the code still crashes. When we write touser.idoruser1.name, the compiler enforces correctness:idmust be anu32andnamemust be a[]const u8. Similarly, when we writenulltonode1.next, the code compiles becausenullis a valid?*Node. But when, at runtime, we try to interpret thatnullas a part of aUser, the behavior becomes undefined (i.e. we'll most likely crash).
-
-
Cautions :
-
One last thing worth pointing out is that, unless a structure is declared as
packed, Zig makes no guarantee about its memory layout. -
In almost all cases, you should not write to memory as one type and read it as another (which is exactly what we've done throughout the post).
-
Unless the struct is
packedor the struct is very simple, you cannot predict how those read/writes will be interpreted by different types sharing the same memory.
-
Primitives
-
List .
Integers
-
Integers .
const decimal_int: i32 = 98222;
const hex_int: u8 = 0xff;
const another_hex_int: u8 = 0xFF;
const octal_int: u16 = 0o755;
const binary_int: u8 = 0b11110000;
const one_billion: u64 = 1_000_000_000;
const binary_mask: u64 = 0b1_1111_1111;
const permissions: u64 = 0o7_5_5;
const big_address: u64 = 0xFF80_0000_0000_0000;
-
Coercion / Casting :
const expect = @import("std").testing.expect; test "integer widening" { const a: u8 = 250; // This is ok, providing that the new type can fit all of the values that the old type can. const b: u16 = a; const c: u32 = b; try expect(c == a); }const expect = @import("std").testing.expect; test "@intCast" { const x: u64 = 200; const y = @as(u8, @intCast(x)); try expect(@TypeOf(y) == u8); } -
Overflow :
-
Overflows are detectable illegal behaviour.
-
Sometimes, being able to overflow integers in a well-defined manner is a wanted behaviour.
-
.
-
-
Saturation :
-
Values will stick to their lower and upper bounds.
var i: u8 = 200; // "i" is an unsigned 8-bit integer (values: from 0 to 255) i +| 100 == 255 // u8: won't go higher than 255 i -| 300 == 0 // unsigned, won't go lower than 0 i *| 2 == 255 // u8: won't go higher than 255 i <<| 8 == 255 // u8: won't go higher than 255 -
Floats
-
Floats .
const floating_point: f64 = 123.0E+77;
const another_float: f64 = 123.0;
const yet_another: f64 = 123.0e+77;
const hex_floating_point: f64 = 0x103.70p-5;
const another_hex_float: f64 = 0x103.70;
const yet_another_hex_float: f64 = 0x103.70P-5;
const lightspeed: f64 = 299_792_458.000_000;
const nanosecond: f64 = 0.000_000_001;
const more_hex: f64 = 0x1234_5678.9ABC_CDEFp-10;
-
Coercion / Casting :
-
Floats coerce to larger float types.
const expect = @import("std").testing.expect; test "float widening" { const a: f16 = 0; const b: f32 = a; const c: f128 = b; try expect(c == @as(f128, a)); }-
@floatFromInt-
Is always safe
-
-
@intFromFloat-
Is detectable illegal behaviour if the float value cannot fit in the integer destination type.
-
const expect = @import("std").testing.expect; test "int-float conversion" { const a: i32 = 0; const b = @as(f32, @floatFromInt(a)); const c = @as(i32, @intFromFloat(b)); try expect(c == a); } -
Generics
types
-
A function can return any type, not just primitives and arrays.
-
typesmust always be compile-time known.
Examples
-
Returns an Array (new array type) :
const std = @import("std"); pub fn main() !void { var arr: IntArray(3) = undefined; arr[0] = 1; arr[1] = 10; arr[2] = 100; std.debug.print("{any}\n", .{arr}); } fn IntArray(comptime length: usize) type { return [length]i64; }-
This code only worked because we declared
lengthascomptime. That is, we require anyone who callsIntArrayto pass a compile-time knownlengthparameter.
-
-
Returns a Struct (type) :
const std = @import("std"); pub fn main() !void { var arr: IntArray(3) = undefined; arr.items[0] = 1; arr.items[1] = 10; arr.items[2] = 100; std.debug.print("{any}\n", .{arr.items}); } fn IntArray(comptime length: usize) type { return struct { items: [length]i64, }; } -
Receives a type and returns a Struct (type) :
fn List(comptime T: type) type { return struct { pos: usize, items: []T, allocator: Allocator, fn init(allocator: Allocator) !List(T) { return .{ .pos = 0, .allocator = allocator, .items = try allocator.alloc(T, 4), }; } }; }
Tuples
Tuples
// A tuple is a list of elements, possibly of different types.
const foo = .{ "hello", true, 42 };
// foo.len == 3
Arrays
-
Arrays .
Array (
[N]T
)
const a = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
const b = [_]u8{ 'w', 'o', 'r', 'l', 'd' };
const c: [100]u8 = [_]u8{1} ** 100;
const array = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
const length = array.len; // 5
-
Multidimensional :
const mat4x4 = [4][4]f32{ .{ 1, 0, 0, 0 }, .{ 0, 1, 0, 1 }, .{ 0, 0, 1, 0 }, .{ 0, 0, 0, 1 }, }; // Access the 2D array then the inner array through indexes. try expect(mat4x4[1][1] == 1.0); // Here we iterate with for loops. for (mat4x4) |row, row_index| { for (row) |cell, column_index| { // ... } }
ArrayList (
std.ArrayList(T)
)
-
Serves as a buffer that can change size.
-
Similarities :
-
std.ArrayList(T)is similar to C++'sstd::vector<T>and Rust'sVec<T>.
-
-
Memory :
-
The
deinit()method frees all memory used by the ArrayList. -
Memory can be read from and written to via its slice field -
.items.
-
const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
const ArrayList = std.ArrayList;
const test_allocator = std.testing.allocator;
test "arraylist" {
var list = ArrayList(u8).init(test_allocator);
defer list.deinit();
try list.append('H');
try list.append('e');
try list.append('l');
try list.append('l');
try list.append('o');
try list.appendSlice(" World!");
try expect(eql(u8, list.items, "Hello World!"));
}
Vectors
-
Allow efficient parallel operations using SIMD (Single Instruction, Multiple Data) instructions.
-
A data type that stores multiple values of the same type.
-
Vectors can only have child types of booleans, integers, floats and pointers.
-
-
Note that using explicit vectors may result in slower code if you make wrong choices. The compiler's auto-vectorization is fairly smart.
-
Access :
-
Vectors are indexable.
const expect = @import("std").testing.expect; test "vector indexing" { const x: @Vector(4, u8) = .{ 255, 0, 255, 0 }; try expect(x[0] == 255); } -
-
Operations :
-
Addition :
const expect = @import("std").testing.expect; const meta = @import("std").meta; test "vector add" { const x: @Vector(4, f32) = .{ 1, -10, 20, -1 }; const y: @Vector(4, f32) = .{ 2, 10, 0, 1 }; const z = x + y; try expect(meta.eql(z, @Vector(4, f32){ 3, 0, 20, 0 })); }const a: @Vector(4, i32) = @Vector(4, i32){ 1, 2, 3, 4 }; const b: @Vector(4, i32) = @Vector(4, i32){ 10, 20, 30, 40 }; const c = a + b; // Result: {11, 22, 33, 44} -
Scalar Multiply :
-
The function
@splat(len, value)creates a vector filled with the same value.
const v: @Vector(4, i32) = @Vector(4, i32){ 2, 4, 6, 8 }; const scale = 2; const result = v * @splat(4, scale); // {4, 8, 12, 16} -
-
-
Coercion :
-
Vectors coerce to their respective arrays.
const arr: [4]f32 = @Vector(4, f32){ 1, 2, 3, 4 }; -
Strings
// Simple string constant.
const greetings = "hello";
// ... which is equivalent to:
const greetings: *const [5:0]u8 = "hello";
// In words: "greetings" is a constant value, a pointer to a constant array of 5 elements (8-bit unsigned integers), with an extra '0' at the end.
// The extra "0" is called a "sentinel value".
print("string: {s}\n", .{greetings});
-
"There are no strings in Zig, everything is an array of u8. It's frustrating."
-
[]const u8
-
-
Tutorial .
-
Tutorial .
-
Tutorial .
-
RAGE AGAINST THE DYING OF THE LIGHT..................................
String Literals
-
The type of string literals is
*const [N:0]u8, where N is the length of the string.-
This allows string literals to coerce to sentinel-terminated slices, and sentinel-terminated many pointers.
-
const expect = @import("std").testing.expect;
test "string literal" {
try expect(@TypeOf("hello") == *const [5:0]u8);
}
Concatenation
-
With Alloc :
const std = @import("std"); pub fn main() !void { const name = "Leto"; const say = std.fmt.allocPrint(allocator, "Hello {s}", .{name}); defer allocator.free(say); std.debug.print("{s}\n", .{greeting}); -
With buffer :
-
This API moves the memory management burden to the caller. If we had a longer
name, or a smallerbuf, ourbufPrintcould return aNoSpaceLefterror. -
But there are plenty of scenarios where an application has known limits, such as a maximum name length.
-
In those cases
bufPrintis safer and faster.
const std = @import("std"); pub fn main() !void { const name = "Leto"; var buf: [100]u8 = undefined; const greeting = try std.fmt.bufPrint(&buf, "Hello {s}", .{name}); std.debug.print("{s}\n", .{greeting}); } -
Equal
const internalIcons = "Internal_Icons";
if (std.mem.eql(u8, internalIcons, "Internal_Icons")) continue;
Contains
-
Contains .
Copy
-
Copy .
C Strings
-
[*:0]u8and[*:0]const u8perfectly model C's strings.
const expect = @import("std").testing.expect;
test "C string" {
const c_string: [*:0]const u8 = "hello";
var array: [5]u8 = undefined;
var i: usize = 0;
while (c_string[i] != 0) : (i += 1) {
array[i] = c_string[i];
}
}
Sentinel Termination
-
[N:t]T,[:t]T, and[*:t]T, wheretis a value of the child typeT.
const expect = @import("std").testing.expect;
test "sentinel termination" {
const terminated = [3:0]u8{ 3, 2, 1 };
try expect(terminated.len == 3);
try expect(@as(*const [4]u8, @ptrCast(&terminated))[3] == 0);
// `@ptrCast` is used to perform an unsafe type conversion. This shows us that the last element of the array is followed by a 0 byte.
}
-
Coercion :
-
Sentinel-terminated types coerce to their non-sentinel-terminated counterparts.
const expect = @import("std").testing.expect; test "coercion" { const a: [*:0]u8 = undefined; const b: [*]u8 = a; const c: [5:0]u8 = undefined; const d: [5]u8 = c; const e: [:0]f32 = undefined; const f: []f32 = e; _ = .{ b, d, f }; // ignore unused } -
-
Sentinel Terminated Slicing :
-
Can be used to create a sentinel-terminated slice with the syntax
x[n..m:t], wheretis the terminator value. -
Doing this is an assertion from the programmer that the memory is terminated where it should be. Getting this wrong is detectable illegal behaviour.
const expect = @import("std").testing.expect; test "sentinel terminated slicing" { var x = [_:0]u8{255} ** 3; const y = x[0..3 :0]; _ = y; } -
HashMaps
-
std.StringHashMapandstd.AutoHashMapare just wrappers forstd.HashMap.-
If these two do not fulfill your needs, using
std.HashMapdirectly gives you much more control.
-
AutoHashMap
-
std.AutoHashMap -
Lets you easily create a hash map type from a key type and a value type.
-
These must be initialized with an allocator.
test "hashing" {
const Point = struct { x: i32, y: i32 };
var map = std.AutoHashMap(u32, Point).init(
test_allocator, // refers to `std.testing.allocator`.
);
defer map.deinit();
try map.put(1525, .{ .x = 1, .y = -4 });
try map.put(1550, .{ .x = 2, .y = -3 });
try map.put(1575, .{ .x = 3, .y = -2 });
try map.put(1600, .{ .x = 4, .y = -1 });
try expect(map.count() == 4);
var sum = Point{ .x = 0, .y = 0 };
var iterator = map.iterator();
while (iterator.next()) |entry| {
sum.x += entry.value_ptr.x;
sum.y += entry.value_ptr.y;
}
try expect(sum.x == 10);
try expect(sum.y == -10);
}
-
.fetchPut-
Puts a value in the hash map, returning a value if there was previously a value for that key.
test "fetchPut" { var map = std.AutoHashMap(u8, f32).init( test_allocator, ); defer map.deinit(); try map.put(255, 10); const old = try map.fetchPut(255, 100); try expect(old.?.value == 10); try expect(map.get(255).? == 100); } -
StringHashMap
-
std.StringHashMap -
For when you need strings as keys.
test "string hashmap" {
var map = std.StringHashMap(enum { cool, uncool }).init(
test_allocator,
);
defer map.deinit();
try map.put("loris", .uncool);
try map.put("me", .cool);
try expect(map.get("me").? == .cool);
try expect(map.get("loris").? == .uncool);
}
Enums (
enum {}
)
-
Allow you to define types with a restricted set of named values.
const Direction = enum { north, south, east, west };
const Value = enum(u2) { zero, one, two };
-
Default values :
-
Enum ordinal values start at 0. They can be accessed with the built-in function
@intFromEnum.
const expect = @import("std").testing.expect; const Value = enum(u2) { zero, one, two }; test "enum ordinal value" { try expect(@intFromEnum(Value.zero) == 0); try expect(@intFromEnum(Value.one) == 1); try expect(@intFromEnum(Value.two) == 2); }-
Values can be overridden, with subsequent values continuing from there.
const expect = @import("std").testing.expect; const Value2 = enum(u32) { hundred = 100, thousand = 1000, million = 1000000, next, }; test "set enum ordinal value" { try expect(@intFromEnum(Value2.hundred) == 100); try expect(@intFromEnum(Value2.thousand) == 1000); try expect(@intFromEnum(Value2.million) == 1000000); try expect(@intFromEnum(Value2.next) == 1000001); } -
-
Variables :
-
Enums can also have
varandconstdeclarations. -
These act as namespaced globals and their values are unrelated to instances of the enum type.
const expect = @import("std").testing.expect; const Mode = enum { var count: u32 = 0; on, off, }; test "hmm" { Mode.count += 1; try expect(Mode.count == 1); } -
-
Methods :
const expect = @import("std").testing.expect; const Suit = enum { clubs, spades, diamonds, hearts, pub fn isClubs(self: Suit) bool { return self == Suit.clubs; } }; test "enum method" { try expect(Suit.spades.isClubs() == Suit.isClubs(.spades)); } -
Casting :
-
Enums aren't integers. Convert them with a built-in.
const Value = enum { zero, stuff, blah }; if (@enumToInt(Value.zero) == 0) { ... } if (@enumToInt(Value.stuff) == 1) { ... } if (@enumToInt(Value.blah) == 2) { ... } -
Unions
-
Define types that store one value of many possible typed fields.
-
Only one field may be active at a time.
const Result = union {
int: i64,
float: f64,
bool: bool,
};
test "simple union" {
var result = Result{ .int = 1234 };
result.int = 11; // valid.
result.float = 12.34; // invalid.
}
-
Tagged Unions :
-
Are unions that use an enum to indicate which field is active.
const expect = @import("std").testing.expect; const Tag = enum { a, b, c }; const Tagged = union(Tag) { a: u8, b: f32, c: bool }; test "switch on tagged union" { var value = Tagged{ .b = 1.5 }; switch (value) { // With `|*value|` we can capture a pointer to the values instead of the values themselves, allowing us to use dereferencing to mutate the original value. .a => |*byte| byte.* += 1, .b => |*float| float.* *= 2, .c => |*b| b.* = !b.*, } try expect(value.b == 3); }-
The tag type of a tagged union can also be inferred. This is equivalent to the
Taggedtype above.
const Tagged = union(enum) { a: u8, b: f32, c: bool };-
voidmember types can have their type omitted from the syntax. Here,nonehas typevoid.
const Tagged2 = union(enum) { a: u8, b: f32, c: bool, none }; -
Structs (
T{}
)
-
Zig gives no guarantees about the in-memory order of fields in a struct or its size.
-
Struct fields cannot be implicitly uninitialized. If some component of the Struct is missing initialization, it will cause an error.
const Vec3 = struct { x: f32, y: f32, z: f32 };
test "struct usage" {
const my_vector = Vec3{
.x = 0,
.y = 100,
.z = 50,
};
_ = my_vector;
}
-
Defaults :
const Vec4 = struct { x: f32 = 0, y: f32 = 0, z: f32 = 0, w: f32 = 0 }; test "struct defaults" { const my_vector = Vec4{ .x = 25, .y = -50, }; _ = my_vector; } -
Packed :
// Packed structure, with guaranteed in-memory layout. const Divided = packed struct { half1: u8, quarter3: u4, quarter4: u4, }; -
Methods :
-
"Structs have the unique property that when given a pointer to a struct, one level of dereferencing is done automatically when accessing fields."
-
"In this example,
self.xandself.yare accessed in the swap function without needing to dereference the self pointer."
const expect = @import("std").testing.expect; const Stuff = struct { x: i32, y: i32, fn swap(self: *Stuff) void { const tmp = self.x; self.x = self.y; // "without needing to dereference the self pointer", I believe this is `self.x` in Zig, whereas in C++ it would be `self->x` / `(*self).x`. self.y = tmp; } }; test "automatic dereference" { var thing = Stuff{ .x = 10, .y = 20 }; thing.swap(); try expect(thing.x == 20); try expect(thing.y == 10); }const Point = struct { const Self = @This(); // Refers to its own type (later called "Point"). x: u32, y: u32, // Take a look at the signature. First argument is of type *Self: "self" is // a pointer on the instance of the structure. // This allows the same "dot" notation as in OOP, like "instance.set(x,y)". // See the following example. pub fn set(self: *Self, x: u32, y: u32) void { self.x = x; self.y = y; } // Again, look at the signature. First argument is of type Self (not *Self), // this isn't a pointer. In this case, "self" refers to the instance of the // structure, but can't be modified. pub fn getx(self: Self) u32 { return self.x; } // PS: two previous functions may be somewhat useless. // Attributes can be changed directly, no need for accessor functions. // It was just an example. }; -
-
Anonymous Structs :
const expect = @import("std").testing.expect; test "anonymous struct literal" { const Point = struct { x: i32, y: i32 }; const pt: Point = .{ .x = 13, .y = 67, }; try expect(pt.x == 13); try expect(pt.y == 67); }const expect = @import("std").testing.expect; test "fully anonymous struct" { try dump(.{ .int = @as(u32, 1234), .float = @as(f64, 12.34), .b = true, .s = "hi", }); } fn dump(args: anytype) !void { try expect(args.int == 1234); try expect(args.float == 12.34); try expect(args.b); try expect(args.s[0] == 'h'); try expect(args.s[1] == 'i'); } -
Files as Structs :
-
Using files as structs {11:30} .
-
Important notes:
-
const Point = @This;as the access point. -
Define functions as
pubif you want them accessible outside the file.
-
-
-
@This()
const Tea = struct {
const Self = @This();
};
pub fn main() !void {
// prints "true"
std.debug.print("{}\n", .{Tea == Tea.Self});
}
Declaration
-
"The principal units of code in Zig are declarations, not expressions".
-
In Godot :
const z := Vector2(0, 0) -
Method 'a' :
const a: rl.Vector2 = .{.x = 0, .y = 0};-
Initializes the struct by values.
-
The method is called 'Anonymous Struct'.
-
Caio:
-
When it comes to defining default values in a struct, maybe method
ais okay, but it's still inconvenient.
const MinhaStruct = Struct{ // vel: rl.Vector2 = .{.x = 0, .y = 0}, // vel: rl.Vector2 = rl.Vector2.init(0, 0), }; -
-
-
Method 'b' :
const b = rl.Vector2.init(0, 0);-
Caio:
-
I feel a bit worried about 'initializing the struct by values', as I don't know if I'm initializing all the necessary values of the struct, so I usually go for a
.init-based approach. -
The problem is: I change the struct, but I don't get warnings from the LSP, so the only way I know I'm creating the struct wrong is when compiling. Though, if there is an
initfunction and I'm always using it, it feels easier on the LSP, and I don't need to compile to see the error. -
I'm thinking more about how to avoid having to compile every time to check for type errors in places where I use the struct.
-
-
-
Method 'c' :
const c = rl.Vector2{.x = 0, .y = 0};-
Initializes the struct by values.
-
Quotes:
-
"c is probably least recommended followed by b."
-
"a and b are fine, c is weird".
-
-
-
Method 'd'
const d: rl.Vector2 = .init(0, 0);-
Quotes:
-
"the problem with d is that it doesn't work with
catch".
-
-
Error Handling
undefined, null, void
undefined
-
var foo: u8 = undefined. -
Should not be thought of as no value, but as a way of telling the compiler that you are not assigning a value yet .
-
Any type may be set to undefined, but attempting to read or use that value is always considered a mistake.
null
-
var foo: ?u8 = null;. -
The "null" primitive value is a value that means "no value".
-
This is typically used with optional types as in the example above.
-
When
fooequalsnull, that's not a value of typeu8. It means there is no value of typeu8infooat all.
void
-
var foo: void = {};. -
"void" is a type , not a value.
-
It is the most common of the Zero Bit Types (those types which take up absolutely no space and have only a semantic value).
-
When compiled to executable code, zero bit types generate no code at all.
-
The above example shows a variable
fooof typevoidwhich is assigned the value of an empty expression. -
It's much more common to see
voidas the return type of a function that returns nothing.
Error Sets, Try, Catch
Error Sets
-
An error set is like an enum, where each error in the set is a value.
-
There are no exceptions in Zig; errors are values.
const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
Union: Error
-
An error set type and another type can be combined with the
!operator to form an error union type. -
Values of these types may be an error value or a value of the other type .
-
If you call a function that returns an error union (
!T) without usingtryorcatch, the Zig compiler will emit a compile error, since there is no defined way to handle the possible error. -
anyerror-
Is the global error set, which due to being the superset of all error sets, can have an error from any set coerced to it. Its usage should generally be avoided.
-
-
In variables :
const maybe_error: AllocationError!u16 = 10; // `maybe_error` can be a `u16` or an error of type `AllocationError`. // `AllocationError!u16` means the type can be `AllocationError` or `u16`. const no_error = maybe_error catch 0; // If `maybe_error` contained an error, `no_error` would receive `0`. // Since `maybe_error` does not contain an error, `no_error` equals 10. -
In functions :
-
With AnyError :
fn mightFail(x: bool) !i32 { // `!i32` means the type can be **any error** or `i32`. if (x) { return error.SomeError; } return 42; } -
With ErrorSet :
const MyErrors = error{ OutOfMemory, InvalidInput }; fn example(x: bool) MyErrors!i32 { if (x) { return error.OutOfMemory; // valid. //return error.InvalidInput; // valid. //return error.SomeOtherError; // invalid. } return 42; }
-
-
Merge :
const A = error{ NotDir, PathNotFound }; const B = error{ OutOfMemory, PathNotFound }; const C = A || B;
Try
-
Used to propagate errors automatically .
-
If the operation results in an error, the error will be returned to the caller.
-
-
Note :
-
Zig's
tryandcatchare unrelated to try-catch in other languages. -
Zig does not let us ignore error unions via
_ = x;. We must unwrap it withtry,catch, orifby some means.-
_ = try x;or_ = x catch {};is possible.
-
-
-
Syntax sugar for
|err|:-
try xis shorthand forx catch |err| return err.
-
Catch
-
catchis used to handle errors directly , providing an alternative value or specific handling.-
In other words, a 'fallback value'.
-
"Could instead be a
noreturn- the type ofreturn,while (true)and others."
-
-
Basic :
const result = mightFail(false) catch -1; // If there is an error, result is -1. -
With Payload Capturing :
fn failingFunction() error{Oops}!void { return error.Oops; } fn main() !void { failingFunction() catch |err| { return; }; } -
With Payload Capturing and Blocks :
-
"If you want to provide a default value with
catchafter performing some logic, you can combinecatchwith named Blocks :" -
Source .
const a: ?std.json.Parsed(std.json.Value) = parseJson(allocator, "mapa/mapa.ldtkasd") catch |err| blk: { print("File not found {}\n", .{err}); break :blk null; }; -
-
With Payload Capturing and Switch :
fn mightFail(x: bool) !i32 { if (x) return 42; return error.SomeError; } pub fn main() void { const result = mightFail(false) catch |err| switch (err) { error.SomeError => -1, // Convert the error to -1 else => -999, // Capture other errors }; } -
My examples :
-
Potentially crashing the program if an error occurs:
const jsonParsed = try parseJson(allocator, mapa); defer jsonParsed.deinit(); -
Trying to continue program execution if an error occurs:
const jsonParsed: ?std.json.Parsed(std.json.Value) = parseJson(allocator, mapa) catch |err| a: { print("\nERROR | LDtkParser: {}, {s}\n", .{err, mapa}); break :a null; }; if (jsonParsed) |*jsonParsed_| { defer jsonParsed_.*.deinit(); //etc. }
-
Optionals
Union: Optionals (
?T
)
-
Used to store either
nullor a value of typeT. -
Unwrapping :
-
.?is a shorthand fororelse unreachable.-
This is used when you know it is impossible for an optional value to be null; using this to unwrap a
nullvalue is detectable illegal behaviour.
const expect = @import("std").testing.expect; test "orelse unreachable" { const a: ?f32 = 5; const b = a orelse unreachable; const c = a.?; try expect(b == c); try expect(@TypeOf(c) == f32); } -
-
orelse:-
Acts when the optional is
null. This unwraps the optional to its child type.
const expect = @import("std").testing.expect; test "orelse" { const a: ?f32 = null; const fallback_value: f32 = 0; const b = a orelse fallback_value; try expect(b == 0); try expect(@TypeOf(b) == f32); } -
-
Unwrapping in expressions and loops :
-
If :
const a: ?i32 = 5; // Method 1 if (a != null) { const value = a.?; _ = value; } // Method 2 var b: ?i32 = 5; if (b) |*value| { value.* += 1; } -
While :
var numbers_left: u32 = 4; fn eventuallyNullSequence() ?u32 { if (numbers_left == 0) return null; numbers_left -= 1; return numbers_left; } fn main() !void { var sum: u32 = 0; while (eventuallyNullSequence()) |value| { sum += value; } } -
As in the union example, the captured value is immutable, but we can still use a pointer capture to modify the value stored in
b.
-
-
Note :
-
Optional pointer and optional slice types do not take up any extra memory compared to non-optional ones.
-
This is because internally they use the 0 value of the pointer for
null.
-
-
Runtime Safety, Unreachable
Detectable illegal behaviour
-
Illegal behaviour will be caught (causing a panic) with safety on, but will result in undefined behaviour with safety off.
-
Users are strongly recommended to develop and test their software with safety on, despite its speed penalties.
-
Enabled:
test "out of bounds" { const a = [3]u8{ 1, 2, 3 }; var index: u8 = 5; const b = a[index]; _ = b; index = index; } -
Disabled:
test "out of bounds, no safety" { @setRuntimeSafety(false); const a = [3]u8{ 1, 2, 3 }; var index: u8 = 5; const b = a[index]; _ = b; index = index; }
Unreachable
-
unreachableis an assertion to the compiler that this statement will not be reached. -
It can tell the compiler that a branch is impossible, which the optimiser can then take advantage of.
-
Reaching an
unreachableis detectable illegal behaviour.
test "unreachable" {
const x: i32 = 1;
const y: u32 = if (x == 2) 5 else unreachable; // crashes if `unreachable` is reached.
_ = y;
}
const expect = @import("std").testing.expect;
fn asciiToUpper(x: u8) u8 {
return switch (x) {
'a'...'z' => x + 'A' - 'a',
'A'...'Z' => x,
else => unreachable, // crashes if `unreachable` is reached.
};
}
test "unreachable switch" {
try expect(asciiToUpper('a') == 'A');
try expect(asciiToUpper('A') == 'A');
}
Memory
-
Memory .
Lifetime and Ownership
-
Ownership determines whose responsibility it is to free the memory referenced by the pointer, and lifetime determines the point at which the memory becomes inaccessible.
-
It is the Zig programmer's responsibility to ensure that a pointer is not accessed when the memory pointed to is no longer available.
-
Note that a slice is a form of pointer, in that it references other memory.
-
Conventions :
-
In general, when a function returns a pointer, the documentation for the function should explain who "owns" the pointer. This concept helps the programmer decide when it is appropriate, if ever, to free the pointer.
-
For example, the function's documentation may say "caller owns the returned memory", in which case the code that calls the function must have a plan for when to free that memory.
-
Probably in this situation, the function will accept an
Allocatorparameter. -
The API documentation for functions and data structures should take great care to explain the ownership and lifetime semantics of pointers.
-
-
Defer
Defer
-
Defer is used to execute a statement upon exiting the current block.
-
When there are multiple defers in a single block, they are executed in reverse order.
const expect = @import("std").testing.expect;
test "defer" {
var x: i16 = 5;
{
defer x += 2;
try expect(x == 5); // first the test runs, then the defer happens.
}
try expect(x == 7);
}
const expect = @import("std").testing.expect;
test "multi defer" {
var x: f32 = 5;
{
defer x += 2; // runs after this one.
defer x /= 2; // runs first.
}
try expect(x == 4.5);
}
const std = @import("std");
const expect = std.testing.expect;
const print = std.debug.print;
test "defer unwinding" {
print("\n", .{});
defer {
print("1 ", .{});
}
defer {
print("2 ", .{});
}
if (false) {
// defers are not run if they are never executed.
defer {
print("3 ", .{});
}
}
}
-
Example of handling Optionals (
?T) :const jsonParsed: ?std.json.Parsed(std.json.Value) = parseJson(allocator, mapa) catch |err| blk: print("\nERROR | LDtkParser: {}, {s}\n", .{err, mapa}); break :blk null; };-
Correct :
defer { if (jsonParsed != null) { jsonParsed.?.deinit(); } // or if (jsonParsed) |jsonParsed_| { defer jsonParsed_.deinit(); } // or, (not sure which is correct) if (jsonParsed) |*jsonParsed_| { defer jsonParsed_.*.deinit(); } }-
The
deferwill happen at the expected moment, performing actions depending on whether the variable is null.
-
-
Incorrect :
if (jsonParsed != null) { defer jsonParsed.?.deinit(); } // or if (jsonParsed) |jsonParsed_| { defer jsonParsed_.deinit(); } // or, (not sure which is correct) if (jsonParsed) |*jsonParsed_| { defer jsonParsed_.*.deinit(); }-
All syntaxes are valid, but the
deferwill run as soon as theifscope exits, i.e., immediately. Thedeferis executed inside theif, not outside it.
-
-
-
Comparing with Go :
-
Zig's
deferis similar to Go's, with one major difference. -
In Zig, the defer runs at the end of its containing scope.
-
In Go, defer runs at the end of the containing function.
-
Zig's approach is probably less surprising, unless you are a Go developer.
-
errdefer
-
errdeferworks likedefer, but only executes when the function returns with an error inside theerrdefer's block.
var problems: u32 = 98;
fn failingFunction() error{Oops}!void {
return error.Oops;
}
fn failFnCounter() error{Oops}!void {
errdefer problems += 1;
try failingFunction();
}
fn main() !void {
failFnCounter() catch |err| {
return;
};
}
-
Ex1 :
const std = @import("std"); const Allocator = std.mem.Allocator; pub const Game = struct { players: []Player, history: []Move, allocator: Allocator, fn init(allocator: Allocator, player_count: usize) !Game { var players = try allocator.alloc(Player, player_count); errdefer allocator.free(players); // store 10 most recent moves per player var history = try allocator.alloc(Move, player_count * 10); return .{ .players = players, .history = history, .allocator = allocator, }; } fn deinit(game: Game) void { const allocator = game.allocator; allocator.free(game.players); allocator.free(game.history); } };-
Under normal conditions,
playersis allocated ininitand released indeinit. But there's an edge case when the initialization ofhistoryfails. In this case and only this case we need to undo the allocation ofplayers. -
Another notable aspect is that the lifecycle of our two dynamically allocated slices,
playersandhistory, is based on application logic. There's no rule that dictates whendeinitmust be called or who must call it. This is good because it gives arbitrary lifetimes, but bad because we can forget to calldeinitor call it more than once.
-
Comptime
-
"Compile time" is a program's environment while it is being compiled.
-
"Run time" is the environment while the compiled program executes.
-
All compiled languages perform some logic at compile time to analyze code and build symbol tables.
-
Optimizations :
-
Compilers can precompute or inline things at compile time to make the resulting program more efficient.
-
Smart compilers can even unroll loops.
-
Zig makes compile-time execution an integral part of the language.
-
-
Zig has a powerful
comptimefeature to do things at compile time. Compile-time execution can only operate on compile-time known data. Zig providescomptime_intandcomptime_floattypes. Example:var x = 0; while (true) { if (someCondition()) break; x += 2; }-
This won't compile.
x's type is inferred as acomptime_intsince the value0is known at compile time. Acomptime_intmust be aconst. If we change toconst x = 0;we'll get a different error because we try to add 2 to aconst. -
The solution is to explicitly define
xas a runtime integer type:var x: usize = 0;
-
Numeric Literals
-
ALL numeric literals in Zig are of type
comptime_intorcomptime_float. They are arbitrary precision.
const const_int = 12345;
const const_float = 987.654;
-
When assigned to
constidentifiers, we don't need to specify sizes likeu8orf64. -
The values are inserted at compile time. The identifiers
const_intandconst_floatdon't exist in the compiled binary.
Pointers
-
Pointers .
Single-item Pointer (
*T
)
-
Normal pointers in Zig cannot have 0 or null as a value.
-
Setting a
*Tto 0 is detectable illegal behaviour.
-
-
Referencing is
&variable, dereferencing isvariable.*.
const expect = @import("std").testing.expect;
// The function receives a pointer to `u8`.
fn increment(num: *u8) void {
num.* += 1;
// `num.*` accesses the value pointed to by the pointer (dereference).
}
test "pointers" {
var x: u8 = 1;
increment(&x); // Pass a pointer to `x` to `increment`.
try expect(x == 2);
}
-
Sizes :
-
usizeandisizehave the same size as pointers.
test "usize" { try expect(@sizeOf(usize) == @sizeOf(*u8)); try expect(@sizeOf(isize) == @sizeOf(*u8)); } -
-
Coercion / Casting :
-
Pointers are not integers; explicit conversion is needed.
-
-
Recommendations :
-
Prefer slices and array types to raw pointers. Compiler-enforced types are less error-prone than pointer manipulation.
-
Many-item Pointer (
[*]T
)
-
Many pointer types exist to represent what is pointed to: single value or array, known length or not.
-
Most programs need buffers with runtime-known lengths. Many-item pointers represent those.
-
Questions :
-
Example usage confusion:
const expect = @import("std").testing.expect; fn doubleAllManypointer(buffer: [*]u8, byte_count: usize) void { var i: usize = 0; while (i < byte_count) : (i += 1) buffer[i] *= 2; } test "many-item pointers" { var buffer: [100]u8 = [_]u8{1} ** 100; const buffer_ptr: *[100]u8 = &buffer; const buffer_many_ptr: [*]u8 = buffer_ptr; doubleAllManypointer(buffer_many_ptr, buffer.len); for (buffer) |byte| try expect(byte == 2); const first_elem_ptr: *u8 = &buffer_many_ptr[0]; const first_elem_ptr_2: *u8 = @ptrCast(buffer_many_ptr); try expect(first_elem_ptr == first_elem_ptr_2); } -
"Slices can be thought of as many-item pointers (
[*]T) plus a length (usize)."
-
Slices (
[]T
)
-
Slices vs Arrays :
-
Slices do not store data, only a reference to the original array.
-
They store the valid length of the buffer.
-
-
Slices can have runtime variable length; arrays have fixed length known at compile time.
-
-
Slices vs Many-item Pointers :
-
Slices are safer and more convenient.
forloops work on slices.
-
-
Slices are "fat pointers" and are typically twice the size of a normal pointer.
-
Slicing :
-
Create from an array with
x[n..m]. -
Slicing includes
nand excludesm.
const expect = @import("std").testing.expect; fn total(values: []const u8) usize { var soma: usize = 0; for (values) |v| soma += v; return soma; } test "slices" { const array = [_]u8{ 1, 2, 3, 4, 5 }; const slice = array[0..3]; // elements 0, 1 and 2. try expect(total(slice) == 6); // returns 6 = 1 + 2 + 3. try expect(@TypeOf(slice) == *const [3]u8); }-
Use
x[n..]to slice to the end.
test "slices 3" { var array = [_]u8{ 1, 2, 3, 4, 5 }; var slice = array[0..]; _ = slice; } -
Pointer Types
-
Single-item Pointer vs Multi-item Pointers :
-
.
-
-
.
-
[]Tis a Slice.
-
Dangling Pointers
-
About :
-
Returning the address of a local.
-
-
Ex1 :
const std = @import("std"); pub fn main() !void { const warning1 = try powerLevel(9000); const warning2 = try powerLevel(10); std.debug.print("{s}\n", .{warning1}); std.debug.print("{s}\n", .{warning2}); } fn powerLevel(over: i32) ![]u8 { var buf: [20]u8 = undefined; return std.fmt.bufPrint(&buf, "over {d}!!!", .{over}); }-
Here we return the address of
buf, butbufceases to exist when the function returns.
-
-
Ex2 :
-
Other examples:
-
Arena allocator created inside a struct, etc.
-
Not fully understood.
-
-
Printing a pointer that pointed to a StringHashMap entry that was removed.
-
A simple, somewhat silly example.
-
-
-
Ex3 :
const std = @import("std"); pub fn main() void { const user1 = User.init(1, 10); const user2 = User.init(2, 20); std.debug.print("User {d} has power of {d}\n", .{user1.id, user1.power}); std.debug.print("User {d} has power of {d}\n", .{user2.id, user2.power}); } pub const User = struct { id: u64, power: i32, fn init(id: u64, power: i32) *User{ var user = User{ .id = id, .power = power, }; return &user; } };-
The problem is
User.initreturns the address of the localuser. That's a dangling pointer. Returning&userreturns an invalid address. -
A simple fix is to change
initto returnUser(not*User) andreturn user;.-
But that's not always possible.
-
Data often must outlive function scope. For that we use the heap.
-
-
-
Ex4 :
fn read() !void { const input = try readUserInput(); return Parser.parse(input); }-
If
Parser.parsereturns a value that referencesinput, that will be a dangling pointer. IdeallyParserwould copyinputif it needs it to live longer. There's nothing here to enforce that. Check documentation or source to know semantics.
-
Memory: Allocators
Injecting the Allocator
-
One of Zig's core principle is no hidden memory allocations .
-
It's a sharp contrast to what you'll find in C where memory is allocated with the standard library's
mallocfunction. -
In C, if you want to know whether or not a function allocates memory, you need to read the source and look for calls to
malloc.
-
-
The advantage of injecting the allocator isn't just explicitness, it's also flexibility.
-
std.mem.Allocatoris an interface which provides thealloc,free,createanddestroyfunctions, along with a few others.
-
-
If you're building a library, then it's best to accept an
std.mem.Allocatorand let users of your library decide which allocator implementation to use. Otherwise, you'll need to chose the right allocator. -
Related notes when using std.json :
-
Caio: "is it a good idea to only use 1 allocator across the whole game? I don't know if this is even possible, but purely talking in terms of a centralized way of allocating and deallocating memory"
-
yes. you might end up wanting two allocators (one for general memory that you manage the lifetime of, and a seperate arena allocator that frees all its memory every frame)
-
-
Caio: "seems like the function is returning a huge bag of unwanted data. I mean, all I actually want is the obj, as it is inside of it that the json data is stored. Is there a way to only return the obj and not have a leak?
-
"if memory is allocated within the function, then you need a way for the caller to free it. so with std.json.parseFromSlice(), that's by calling .deinit() on the returned value. that's a pretty common pattern."
-
Caio: "if memory is allocated inside a function, return the object allocated".
-
-
"For a json parsing, the Parsed(T) includes an ArenaAllocator and the value - that's it. The ArenaAllocator holds all the memory for everything inside value".
-
Areas of memory
-
The areas are conceptual; OS and executable enforce them.
-
Global space .
-
Stack .
-
Heap .
Global Space
-
The first is global space, which is where program constants, including string literals, are stored.
-
All global data is baked into the binary, fully known at compile time (and thus runtime) and immutable.
-
This data exists throughout the lifetime of the program, never needing more or less memory.
-
Aside from the impact it has on the size of our binary, this isn't something we need to worry about at all.
Stack Allocator
-
Explanation of Allocators, focusing on Linear Allocators (Stack) .
-
Does not talk about the different types of allocators, only the Linear (Stack).
-
Provides a good visualization of how the Stack is used in functions.
-
The video is good, but not very detailed.
-
-
Advantages :
-
The call stack is amazing because of the simple and predictable way it manages data (by pushing and popping stack frames).
-
Automatically handled by the compiler.
-
Very fast allocation and cleanup.
-
-
Constraints :
-
Fixed total memory.
-
"You are not allowed to store GBs of memory on the stack, for example".
-
-
Fixed size.
-
Fixed lifetimes.
-
Data has a lifetime tied to its place on the call stack.
-
-
Heap Allocator
-
Useful for data that has to live beyond the rigid boundaries of function scopes.
-
We can create memory at runtime with a runtime-known size and have complete control over its lifetime.
-
It has no built-in life cycle, so our data can live for as long or as short as necessary. And that benefit is its drawback: it has no built-in life cycle, so if we don't free data, no one will.
-
You can allocate memory in an HTTP handler and free it in a background thread, two completely separate parts of the code.
-
-
Everything we've seen so far has been constrained by requiring an upfront size. Arrays always have a compile-time known length (in fact, the length is part of the type). All of our strings have been string literals, which have a compile-time known length.
-
Furthermore, the two types of memory management strategies we've seen, global data and the call stack, while simple and efficient, are limiting. Neither can deal with dynamically sized data and both are rigid with respect to data lifetimes.
Strategies: Heap Allocation
-
Different Allocator Strategies in Zig .
-
At the time, GPA (DebugAllocator) did not exist.
-
The conclusion of the video was that "This is a developing area, but Zig is doing very well here, because it has no default allocator and forces you to think about allocator choice."
-
It wasn’t discussed which allocator to use in each case; it was only about strategies.
-
It’s strange how this area still feels so "new".
-
Page Allocator ("using syscalls")
-
std.heap.page_allocator;. -
Allocates a whole page of memory each time we ask for some memory.
-
Whenever this allocator makes an allocation, it will ask your OS for entire pages of memory; an allocation of a single byte will likely reserve multiple kibibytes.
-
As asking the OS for memory requires a system call, this is also extremely inefficient for speed.
-
Very simple, very dumb, very wasteful.
-
Disadvantages :
-
"This is the base of most allocators, but it's not what people use directly".
-
Very slow, since it uses syscalls; "massive slow in your program".
-
Wasteful.
-
It doesn’t think in terms of bytes, but pages (4KB).
-
-
-
Examples :
const std = @import("std"); fn main() !void { const allocator = std.heap.page_allocator; const memory = try allocator.alloc(u8, 100); // we allocate 100 bytes as a `[]u8`. defer allocator.free(memory); // defer is used in conjunction with a free - this is a common pattern for memory management in Zig. } -
Construction? :
const PageAllocator = struct { pub fn alloc(self: *@This(), size: u32) []u8 { const mem = std.os.mmap( // slow (syscall) alignForward(size, page_size) ) catch { return error.OutOfMemory; } return mem[0..size]; } pub fn free(self: *@This(), mem: []u8) void { return std.os.munmap(mem); } }
FixedBufferAllocator ("Bump Allocator")
-
std.heap.FixedBufferAllocator.init(...); -
Is an allocator that allocates memory into a fixed buffer and does not make any heap allocations.
-
Uses a fixed buffer to get its memory, doesn’t ask memory from the kernel.
-
It will give you the error
OutOfMemoryif it has run out of bytes. -
Advantages :
-
Very fast allocation.
-
Control lifetime via buffer.
-
-
Disadvantages :
-
Fixed total memory.
-
Cannot free individual memory.
-
"There is no data structure, it only stores the last memory index. Therefore, you can’t deallocate memory in the middle of this region."
-
"Maybe it’s possible to deallocate the last allocation."
-
"It’s possible to clear the whole buffer."
-
freeanddestroywill only work on the last allocated/created item (think of a stack).-
Freeing the non-last allocation is safe to call, but won’t do anything.
-
-
-
-
When to use :
-
This is useful when heap usage is not wanted, for example, when writing a kernel.
-
It may also be considered for performance reasons.
-
"If you don’t care about expandable memory, you should use FixedBufferAllocator, as it’s simply faster."
-
"Probably the fastest you’ll ever get."
-
-
Examples :
const std = @import("std"); fn main() !void { var buffer: [1000]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buffer); const allocator = fba.allocator(); const memory = try allocator.alloc(u8, 100); defer allocator.free(memory); }
ArenaAllocator ("Bump Allocator with expandable memory")
-
std.heap.ArenaAllocator.init(...); -
It’s the place where you store all data that share the same lifetime.
-
Takes in a child allocator and allows you to allocate many times and only free once. Use in combination with another allocator.
-
Here,
.deinit()is called on the arena, which frees all memory.-
Using
allocator.freein this example would be a no-op (i.e., does nothing).
-
-
Advantages :
-
Very fast allocation.
-
Expandable total memory.
-
Manual lifetime.
-
"Arena = One Lifetime".
-
-
Very simple way of avoiding leaks.
-
-
Disadvantages :
-
Cannot free individual memory.
-
"This ends up being useful for cases like Linked Lists, for example, since it allows freeing the entire list’s memory at once without traversing it."
-
Disclaimer: "Don’t use Linked Lists, use arrays. Arrays are much faster nowadays."
-
-
-
When to use :
-
Commonly used in some places, but the problem of not being able to "free individual memory" can be annoying in some cases.
-
-
Examples :
const std = @import("std"); fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); _ = try allocator.alloc(u8, 1); _ = try allocator.alloc(u8, 10); _ = try allocator.alloc(u8, 100); }
DebugAllocator (formerly GeneralPurposeAllocator (GPA))
-
std.heap.DebugAllocator(.{}){};. -
"In debug builds, use DebugAllocator, formerly known as GPA. In release builds, use std.heap.smp_allocator."
-
"DebugAllocator and smp_allocator are both backed by page_allocator, which requests more memory from the operating system when it runs out."
-
Advantages :
-
Designed for safety over performance, but may still be many times faster than page_allocator.
-
This is a safe allocator that can prevent double-free, use-after-free, and detect leaks.
-
Safety checks and thread safety can be turned off via its configuration struct.
-
-
Thread-safe allocator.
-
Gets some memory first and manages buckets of memory to reduce the number of allocations.
-
-
Uses :
-
Can serve as your application's main allocator. For many programs, this will be the only allocator needed.
-
-
Example :
const std = @import("std"); const httpz = @import("httpz"); pub fn main() !void { // create our general purpose allocator var gpa = std.heap.GeneralPurposeAllocator(.{}){}; // get an std.mem.Allocator from it const allocator = gpa.allocator(); // pass our allocator to functions and libraries that require it var server = try httpz.Server().init(allocator, .{.port = 5882}); var router = server.router(); router.get("/api/user/:id", getUser); // blocks the current thread try server.listen(); }-
(2025-03-27)
var debugAllocator = std.heap.DebugAllocator(.{}){}; // Creates the DebugAllocator TYPE with configuration (.{}) // Using this type, an instance is obtained by {}; // debugAllocator is now an instance of the object `DebugAllocator(...)`. // It’s important that `debugAllocator` is a VAR. If you use CONST, everything crashes at .allocator() below. const allocator = debugAllocator.allocator(); // The internal function is used to obtain the allocator. // allocator is of type `Allocator`. defer { _ = debugAllocator.deinit(); // Not sure exactly why, but it’s important to call .deinit() on the debugAllocator. // Interestingly, the page_allocator does not require this. } -
What is this:
GeneralPurposeAllocator(.{}){}?-
std.heap.GeneralPurposeAllocatoris a function, and since it uses PascalCase, we know it returns a type. -
.{}is a struct initializer with an implicit type. What’s the type and where are the fields? The type isstd.heap.general_purpose_allocator.Config, though it isn’t directly exposed like this, which is one reason we aren’t explicit. No fields are set because theConfigstruct defines defaults, which we’ll be using. -
This is a common pattern with configuration / options.
-
-
SMP Allocator
-
?
-
Suggested for Release builds.
Testing Allocator
-
std.testing.allocator -
About :
-
This is a special allocator that only works in tests and can detect memory leaks.
-
Currently, it’s implemented using the
GeneralPurposeAllocatorwith added integration in Zig’s test runner, but that’s an implementation detail. -
The important thing is that if we use
std.testing.allocatorin our tests , we can catch most memory leaks.
-
-
In your code, use whatever allocator is appropriate.
Discussion: Slab Allocator
-
Similar to the Arena Allocator.
-
Advantages :
-
You can manually free memory.
-
-
Disadvantages :
-
Allocations have fixed sizes.
-
Metadata storage is wasteful.
-
Discussion: General Purpose Allocator
-
About :
-
This type of allocator was discussed in this Zig talk from June 2020. There was no GPA yet, so everything discussed in the video and the section below is speculative.
-
Not sure if DebugAllocator / GPA is related to this concept.
-
Still, the strategic discussion is interesting.
-
-
Free lists :
-
Advantages :
-
You can manually free memory.
-
-
Disadvantages :
-
Allocations have a minimum size.
-
Very slow.
-
Memory Fragmentation.
-
"Worse performance the longer your program is running".
-
"There’s no way to defragment your memory, as there are pointers going everywhere and you can’t really track them down".
-
-
const FreeListAllocator = struct { root: ?*Node, fn find(self: *@This(), size: u32) ?[]u8 { var iter = self.root; while (iter) |node| : (iter = node.next) { if (node.size == size) { self.remove(node); return node.buffer(); } } return null; } pub fn free(self: *@This(), mem: u32) void { const node = Node.init(mem); self.prepend(node); } } -
-
Free lists with size buckets :
-
This solves the Fragmentation problem, since allocations have fixed sizes; kinda; "mitigated, not all gone".
-
Advantages :
-
You can manually free memory.
-
-
Disadvantages :
-
Allocations have a fixed size.
-
Cache pressure.
-
"You’ll probably have cache misses if you’re allocating sporadically".
-
If everything is allocated at once, there might not be cache misses, but if allocations happen occasionally, cache misses will likely occur.
-
This makes sense when you consider that although fragmentation is avoided, this solution ends up spreading allocations that happen after deallocations.
-
"This is really bad".
-
-
-
init(), deinit(), create(), destroy()
-
For slices: use
allocandfree. -
For single items: use
createanddestroy.const std = @import("std"); const expect = std.testing.expect; test "allocator create/destroy" { const byte = try std.heap.page_allocator.create(u8); defer std.heap.page_allocator.destroy(byte); byte.* = 128; }
Warnings
Double Free
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arr = try allocator.alloc(usize, 4);
allocator.free(arr);
allocator.free(arr);
std.debug.print("This won't get printed\n", .{});
}
-
In the case of a double free, we’ll get a hard crash.
Memory Leak
fn isSpecial(allocator: Allocator, name: [] const u8) !bool {
const lower = try allocLower(allocator, name);
return std.mem.eql(u8, lower, "admin");
}
-
The memory created in
allocLoweris never freed. -
Not only that, but once
isSpecialreturns, it can never be freed. OnceisSpecialreturns, we lose our only reference to the allocated memory, thelowervariable. The memory is gone until our process exits.-
Damn.
-
-
Our function might only leak a few bytes, but if it's a long-running process and this function is called repeatedly, it will add up and we'll eventually run out of memory.
-
Memory leaks can be insidious. It isn’t just that the root cause can be difficult to identify. Really small leaks or leaks in infrequently executed code can be even harder to detect.
Threads
-
While Zig provides more advanced ways of writing concurrent and parallel code,
std.Threadis available for making use of OS threads. -
Threads, however, aren't particularly useful without strategies for thread safety.
fn ticker(step: u8) void {
while (true) {
std.time.sleep(1 * std.time.ns_per_s);
tick += @as(isize, step);
}
}
var tick: isize = 0;
test "threading" {
var thread = try std.Thread.spawn(.{}, ticker, .{@as(u8, 1)});
_ = thread;
try expect(tick == 0);
std.time.sleep(3 * std.time.ns_per_s / 2);
try expect(tick == 1);
}
File System
Json
-
-
It's an interesting read.
-
(2025-03-28) I think some things have changed nowadays.
-
-
Json .
-
Poor explanation.
-
-
std.json.-
I prefer to look at the actual std.json file rather than trust that this documentation is up to date.
-
Regex
-
Regex .
Others
Interfaces
-
Zig doesn't have nice syntactic sugar for creating interfaces.
-
One pattern for interface-like behavior are tagged unions, though that's relatively constrained compared to true interfaces.
-
Other patterns have emerged and are used throughout the standard library, such as with
std.mem.Allocator.
Game Dev
Impressions
-
(2025-03-13)
-
I feel like I'm always having to fight the casting system.
-
The system isn't bad, but it's simply VERY verbose and it gets in the way when dealing with APIs, having to do transforms all the time.
-
Making a
util.zigfile and defining helper functions to do the casting helps a lot.
-
-
Compilation times are kind of long.
-
Not absurd, but I feel it can be a problem when dealing with a very large file.
-
So far it's been okay.
-
-
Dealing with strings is super stressful for game dev.
-
When both strings are comptime, concatenation is okay, but when they're runtime you need to work with allocators, etc.
-
This is not a big problem, because I always avoid using strings anyway, besides that although more work, there is elegance in handling strings as
[:0]const u8.
-
-
Allocation is a nightmare.
-
(2025-03-28) Lifetimes are simply very complex, even for something simple.
-
Libraries
Render
-
SDL-Zig .
-
-
Made by the author of Sokol.
-
ECS
Demos
-
-
zig build simple_raytracer-run-
Failed to open, does not exist.
-
-
Demo: Games
-
-
-
Seems friendly :)
-
-
(2025-03-10)
-
I couldn't build it.
-
0.14.0+ is incompatible.
-
I had issues finding dependencies, etc.
-
I managed to open the file downloaded from itch.io
-
-
When testing :
-
The game has a long way to go '-', but at the same time it has a nice charm of a pleasant little game.
-
Reminds me of the feeling of playing a Minecraft alpha at night, kind of cool.
-
The game has no options menu and crashes when trying to resize the window.
-
-
Blockens.-
(2025-03-10)
-
I only managed to build on 1.12.1.
-
The FPS is INSANELY low; like, 4 fps in the start menu and in-game.
-
The CPU got very hot.
-
The game is not worth it at all, for learning or inspiration.
-
-
Mach
About
Impressions
-
I found it a bit uncomfortable to always have a variable called 'mach_system' or 'mech_algo', being an anonymous struct containing method names.
-
From what I understand, this is a way to show Mach which kind of method symbolizes what, so they can be accessed and
runby Mach's API.
-
-
In this case,
pub const mainsymbolizes a scheduler to make certain things be called in order, etc, when calling the "main module" (I think main is the module of this script).-
Since this script is
App.zig, thispub const mainindicates the entry point that themain.zigfile (generated by Mach) will call, once everything starts.
-
-
.
Things made with Mach
-
Games :
-
Aftersun .
-
(2025-03-10)
-
I couldn't build it.
-
0.13.0 is old, 0.14.0+ is incompatible.
-
-
-
-
Pixel art Editor :
-
Pixi .
-
(2025-03-10)
-
I couldn't build it.
-
0.13.0 is old, 0.14.0+ is incompatible.
-
-
-
Godot
Impressions
-
(2025-01-18)
-
Extreme boilerplate. Not doable.
-
Very verbose.
-
I really don't like having to deal with object deinitialization.
-
"Actually I don't like usingnamespace either in its current state, but it's the simplest way I can work out to wrap OOP. The fundamental problem is that Zig doesn’t favor OOP at all, there should be some facilities to assist in doing such".
-
The idea of repeatedly using namespaces sounds strange when looking at the code.
-
-
My feeling is that although Zig is easier to read than Rust, it curiously seems to introduce more boilerplate than Rust, due to the way Godot Zig was implemented. Maybe that will change in the future, but I don't feel it's worth it at all.
-
Tests
-
(2025-01-18)
-
Did not work with Godot 4.4-beta1, but worked with Godot 4.3.
-
I can use the Godot 4.3 .exe and use the .dll in Godot 4.4-beta1 without problems.
-
Steps:
-
Inside
build.zig:
const godot_path = b.option([]const u8, "godot", "Path to Godot engine binary [default: `godot`]") orelse "C:\\Users\\caior\\Desktop\\Programas\\! Godot Launchers\\Godot_v4.3-stable_win64.exe";`-
zig build
-
-